/* tinyproxy - A fast light-weight HTTP proxy
 * Copyright (C) 2003 Steven Young <sdyoung@miranda.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

/* This file contains source code for the handling and display of
 * HTML error pages with variable substitution.
 */

#include "main.h"

#include "common.h"
#include "buffer.h"
#include "conns.h"
#include "heap.h"
#include "html-error.h"
#include "network.h"
#include "utils.h"
#include "conf.h"

/*
 * Add an error number -> filename mapping to the errorpages list.
 */
#define ERRORNUM_BUFSIZE 8      /* this is more than required */
#define ERRPAGES_BUCKETCOUNT 16

int add_new_errorpage(char *filepath, unsigned int errornum) {
	char errornbuf[ERRORNUM_BUFSIZE];

	config.errorpages = hashmap_create(ERRPAGES_BUCKETCOUNT);
	if (!config.errorpages)
		return (-1);

	snprintf(errornbuf, ERRORNUM_BUFSIZE, "%u", errornum);

	if (hashmap_insert(config.errorpages, errornbuf, filepath, strlen(filepath) + 1) < 0)
		return (-1);

	return (0);
}

/*
 * Get the file appropriate for a given error.
 */
static char *
get_html_file(unsigned int errornum) {
	hashmap_iter result_iter;
	char errornbuf[ERRORNUM_BUFSIZE];
	char *key;
	static char *val;

	assert(errornum >= 100 && errornum < 1000);

	if (!config.errorpages)
		return (config.errorpage_undef);

	snprintf(errornbuf, ERRORNUM_BUFSIZE, "%u", errornum);

	result_iter = hashmap_find(config.errorpages, errornbuf);

	if (hashmap_is_end(config.errorpages, result_iter))
		return (config.errorpage_undef);

	if (hashmap_return_entry(config.errorpages, result_iter, &key, (void **) &val) < 0)
		return (config.errorpage_undef);

	return (val);
}

/*
 * Look up the value for a variable.
 */
static char *
lookup_variable(struct conn_s *connptr, const char *varname) {
	hashmap_iter result_iter;
	char *key;
	static char *data;

	result_iter = hashmap_find(connptr->error_variables, varname);

	if (hashmap_is_end(connptr->error_variables, result_iter))
		return (NULL);

	if (hashmap_return_entry(connptr->error_variables, result_iter, &key, (void **) &data) < 0)
		return (NULL);

	return (data);
}

/*
 * Send an already-opened file to the client with variable substitution.
 */
int send_html_file(FILE *infile, struct conn_s *connptr) {
	char *inbuf;
	char *varstart = NULL;
	char *p;
	const char *varval;
	int in_variable = 0;
	int r = 0;

	inbuf = (char *) safemalloc(4096);

	while (fgets(inbuf, 4096, infile) != NULL) {
		for (p = inbuf; *p; p++) {
			switch (*p) {
			case '}':
				if (in_variable) {
					*p = '\0';
					varval = (const char *) lookup_variable(connptr, varstart);
					if (!varval)
						varval = "(unknown)";
					r = write_message(connptr->client_fd, "%s", varval);
					in_variable = 0;
				} else {
					r = write_message(connptr->client_fd, "%c", *p);
				}

				break;

			case '{':
				/* a {{ will print a single {.  If we are NOT
				 * already in a { variable, then proceed with
				 * setup.  If we ARE already in a { variable,
				 * this code will fallthrough to the code that
				 * just dumps a character to the client fd.
				 */
				if (!in_variable) {
					varstart = p + 1;
					in_variable++;
				} else
					in_variable = 0;

			default:
				if (!in_variable) {
					r = write_message(connptr->client_fd, "%c", *p);
				}
			}

			if (r)
				break;
		}

		if (r)
			break;

		in_variable = 0;
	}

	safefree(inbuf);

	return r;
}

int send_http_headers(struct conn_s *connptr, int code, const char *message) {
	const char *headers = "HTTP/1.0 %d %s\r\n"
			"Server: %s/%s\r\n"
			"Content-Type: text/html\r\n" "Connection: close\r\n" "\r\n";

	return (write_message(connptr->client_fd, headers, code, message, PACKAGE, VERSION));
}

/*
 * Display an error to the client.
 */
int send_http_error_message(struct conn_s *connptr) {
	char *error_file;
	FILE *infile;
	int ret;
	const char *fallback_error = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
			"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" "
			"\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"
			"<html>\n"
			"<head><title>%d %s</title></head>\n"
			"<body>\n"
			"<h1>%s</h1>\n"
			"<p>%s</p>\n"
			"<hr />\n"
			"<p><em>Generated by %s version %s.</em></p>\n" "</body>\n"
			"</html>\n";

	send_http_headers(connptr, connptr->error_number, connptr->error_string);

	error_file = get_html_file(connptr->error_number);
	if (!(infile = fopen(error_file, "r"))) {
		char *detail = lookup_variable(connptr, "detail");
		return (write_message(connptr->client_fd, fallback_error, connptr->error_number, connptr->error_string, connptr->error_string, detail, PACKAGE, VERSION));
	}

	ret = send_html_file(infile, connptr);
	fclose(infile);
	return (ret);
}

/*
 * Add a key -> value mapping for HTML file substitution.
 */

#define ERRVAR_BUCKETCOUNT 16

int add_error_variable(struct conn_s *connptr, const char *key, const char *val) {
	if (!connptr->error_variables)
		if (!(connptr->error_variables = hashmap_create(ERRVAR_BUCKETCOUNT)))
			return (-1);

	return hashmap_insert(connptr->error_variables, key, val, strlen(val) + 1);
}

#define ADD_VAR_RET(x, y)				   \
	do {                                               \
                if (y == NULL)                             \
                        break;                             \
		if (add_error_variable(connptr, x, y) < 0) \
			return -1;			   \
	} while (0)

/*
 * Set some standard variables used by all HTML pages
 */
int add_standard_vars(struct conn_s *connptr) {
	char errnobuf[16];
	char timebuf[30];
	time_t global_time;

	snprintf(errnobuf, sizeof errnobuf, "%d", connptr->error_number);
	ADD_VAR_RET("errno", errnobuf);

	ADD_VAR_RET("cause", connptr->error_string);
	ADD_VAR_RET("request", connptr->request_line);
	ADD_VAR_RET("clientip", connptr->client_ip_addr);
	ADD_VAR_RET("clienthost", connptr->client_string_addr);

	/* The following value parts are all non-NULL and will
	 * trigger warnings in ADD_VAR_RET(), so we use
	 * add_error_variable() directly.
	 */

	global_time = time(NULL);
	strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %H:%M:%S GMT", gmtime(&global_time));
	add_error_variable(connptr, "date", timebuf);

	add_error_variable(connptr, "website", "https://banu.com/tinyproxy/");
	add_error_variable(connptr, "version", VERSION);
	add_error_variable(connptr, "package", PACKAGE);

	return (0);
}

/*
 * Add the error information to the conn structure.
 */
int indicate_http_error(struct conn_s *connptr, int number, const char *message, ...) {
	va_list ap;
	char *key, *val;

	va_start(ap, message);

	while ((key = va_arg (ap, char *)))
	{
		val = va_arg (ap, char *);

		if (add_error_variable(connptr, key, val) == -1) {
			va_end(ap);
			return (-1);
		}
	}

	connptr->error_number = number;
	connptr->error_string = safestrdup(message);

	va_end(ap);

	return (add_standard_vars(connptr));
}
